Skip to content

FP8 RL Training and Rollout Weight Synchronization#1898

Open
YJHMITWEB wants to merge 3 commits into
NovaSky-AI:mainfrom
YJHMITWEB:fp8-rl-weight-sync
Open

FP8 RL Training and Rollout Weight Synchronization#1898
YJHMITWEB wants to merge 3 commits into
NovaSky-AI:mainfrom
YJHMITWEB:fp8-rl-weight-sync

Conversation

@YJHMITWEB

@YJHMITWEB YJHMITWEB commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

This PR adds end-to-end FP8 training and rollout support. It covers persistent Megatron FP8 parameters, serialized blockwise weight synchronization, mixed-dtype transport, and FP8 vLLM execution.

Changes

Change Explanation
Serialized blockwise FP8 sync fp8_weight_sync_mode=serialized_blockwise converts eligible weights to E4M3 and sends matching FP32 weight_scale_inv tensors in vLLM checkpoint format.
FP8 vLLM execution Builds the rollout model with quantization=fp8, load_format=dummy, and a matching 128 x 128 block quantization configuration; the first full sync replaces the dummy weights.
Qwen3.5 mapping Handles dense attention and MLP linears, GDN projections, shared experts, and routed MoE experts while keeping incompatible GDN and vision projections in BF16.
Mixed-dtype transport Preserves mixed-dtype logical chunks over NCCL and partitions CUDA IPC buffers by their actual tensor dtype.
Persistent FP8 parameters Keeps eligible Megatron primary parameters in E4M3, reducing the required HBM in Megatron.
Exact optimizer initialization Initializes FP32 masters and hybrid CPU copies from unquantized checkpoint shards before the first optimizer step.
FP8 parameter gather Exposes fp8_param_gather, which refreshes persistent FP8 parameters from updated FP32 masters.
FP8-safe packed sequences Uses one TP/CP-aware alignment rule in collation, packed-sequence preprocessing, and MoE replay.

Scale Support

Without persistent fp8_param, serialized FP8 synchronization supports both FP32 and power-of-two block scales. Hopper supports both modes and defaults to FP32 scales; the current Blackwell path requires power-of-two scales.

Persistent fp8_param is supported only with FP32 scales. The main reason is that at a 1e-6 learning rate, 99.97-99.999% of sampled FP32 master values changed per update, no power-of-two block scale changed. The configuration therefore rejects power-of-two scales with fp8_param. Because Blackwell currently requires power-of-two scales, persistent FP8 parameters are not enabled there.

Configuration

<role> denotes policy or ref. All FP8 features are disabled by default, so existing BF16 configurations retain their current behavior.

Args Default Validated FP8 value Behavior
generator.inference_engine.fp8_weight_sync_mode null serialized_blockwise Sends rollout weights as E4M3 tensors with matching FP32 block scales.
generator.inference_engine.engine_init_kwargs.quantization unset fp8 Enables FP8 vLLM execution.
trainer.<role>.megatron_config.transformer_config_kwargs.fp8 unset e4m3 Enables Transformer Engine FP8 compute for that Megatron model.
trainer.<role>.megatron_config.transformer_config_kwargs.fp8_recipe unset blockwise Selects the Transformer Engine blockwise FP8 recipe.
trainer.<role>.megatron_config.transformer_config_kwargs.fp8_param unset (false) true Stores Megatron primary parameters persistently in E4M3.
trainer.policy.megatron_config.ddp_config.fp8_param_gather false true with policy fp8_param Refreshes policy FP8 parameters from updated FP32 optimizer masters after each step.
Environment variable Runtime default Validated value (Hopper) Behavior
NVTE_FP8_BLOCK_SCALING_FP32_SCALES 1 when FP8 is active 1 1 uses FP32 block scales; 0 uses power-of-two scales. Persistent fp8_param requires 1.
NVTE_FP8_BLOCK_AMAX_EPSILON 0 1e-4 Applies the same non-negative block-amax floor to Transformer Engine and serialized rollout quantization.
VLLM_USE_DEEP_GEMM_E8M0 0 with serialized FP32 scales 0 Prevents vLLM from converting FP32 scales to E8M0 power-of-two scales.

serialized_blockwise requires Megatron and full-weight synchronization; adapter-only Megatron LoRA is rejected. On Blackwell, use power-of-two scales and keep fp8_param disabled.

Performance

vLLM Rollout Throughput

On Qwen/Qwen3.5-9B-Base, NVIDIA H100 80GB, TP1, vLLM 0.23.0, using DAPO recipe dataset.

Concurrent requests BF16 output tok/s FP8 output tok/s Gain
1 140.5 198.6 +41.4%
16 1,940.3 2,599.4 +34.0%
32 3,418.9 4,361.0 +27.6%
128 7,897.7 8,989.2 +13.8%
256 9,223.3 10,791.0 +17.0%

This gain comes from FP8 vLLM execution and does not require persistent FP8 Megatron parameters.

End-to-End RL Step Timing

The end-to-end comparison uses Qwen/Qwen3.5-9B-Base on eight NVIDIA H100 80GB GPUs with Megatron TP2, eight TP1 vLLM engines, and a training batch size of 32.

Phase BF16 mean (s) FP8 mean (s) FP8 time change Speedup
Rollout generation 96.51 72.25 -25.1% 1.34x
Log-probability and reward forward passes 109.87 106.31 -3.2% 1.03x
Policy training 46.50 50.47 +8.5% 0.92x
Policy-to-vLLM weight sync 10.03 10.26 +2.3% 0.98x
End-to-end step 268.90 245.49 -8.7% 1.10x

Persistent FP8 Policy HBM

Megatron primary storage HBM/GPU Change at highest sample
BF16 59,676-63,638 MiB Baseline
Persistent E4M3 56,850-57,810 MiB -5,828 MiB (-5.69 GiB, -9.2%)

Quality Checks

The following WANDB runs log the metrics. All FP8 runs aligns well with the BF16 baseline.
Qwen3.5-4B runs: https://wandb.ai/sky-posttraining-uc-berkeley/qwen35_4b_dapo_mainclean_fp8align_fastbatch_bf16_fp8_20260630?nw=nwuserjinghanyao1

Qwen3.5-9B runs: https://wandb.ai/sky-posttraining-uc-berkeley/qwen35_9b_h100_tp2_bf16_fp8_prready_20260709?nw=nwuserjinghanyao1

Qwen3.5-35B-A3B runs: https://wandb.ai/sky-posttraining-uc-berkeley/qwen35_35b_a3b_fp8_amaxeps_20260705?nw=nwuserjinghanyao1

Reproduce key configs

H100:

export NVTE_FP8_BLOCK_SCALING_FP32_SCALES=1
export NVTE_FP8_BLOCK_AMAX_EPSILON=1e-4
export VLLM_USE_DEEP_GEMM_E8M0=0

trainer.algorithm.kl_loss_coef=0.00 trainer.algorithm.use_kl_in_reward=false \
trainer.policy.model.path=Qwen/Qwen3.5-9B-trainer.placement.colocate_all=true \
trainer.strategy=megatron \
trainer.placement.policy_num_nodes=1 \
trainer.placement.policy_num_gpus_per_node=8 \
trainer.placement.ref_num_gpus_per_node=8 \
trainer.policy.megatron_config.tensor_model_parallel_size=2 \
trainer.policy.megatron_config.pipeline_model_parallel_size=1 \
trainer.policy.megatron_config.context_parallel_size=1 \
trainer.policy.megatron_config.expert_model_parallel_size=1 \
trainer.policy.megatron_config.expert_tensor_parallel_size=1 \
trainer.ref.megatron_config.tensor_model_parallel_size=2 \
trainer.ref.megatron_config.pipeline_model_parallel_size=1 \
trainer.ref.megatron_config.context_parallel_size=1 \
trainer.ref.megatron_config.expert_model_parallel_size=1 \
trainer.ref.megatron_config.expert_tensor_parallel_size=1 \
trainer.epochs=20 trainer.max_training_steps=400 \
trainer.policy.megatron_config.ddp_config.overlap_grad_reduce=false \
trainer.ref.megatron_config.ddp_config.overlap_grad_reduce=false \
trainer.policy.megatron_config.transformer_config_kwargs.fp8_param=true \
trainer.ref.megatron_config.transformer_config_kwargs.fp8_param=true \
trainer.policy.megatron_config.ddp_config.fp8_param_gather=true \
trainer.eval_batch_size=512 \
trainer.train_batch_size=32 \
trainer.policy_mini_batch_size=32 \
trainer.micro_forward_batch_size_per_gpu=1 \
trainer.micro_train_batch_size_per_gpu=1 \
trainer.max_prompt_length=2048 \
trainer.policy.optimizer_config.lr=1e-6 \
trainer.policy.megatron_config.transformer_config_kwargs.fp8=e4m3 \
trainer.ref.megatron_config.transformer_config_kwargs.fp8=e4m3 \
trainer.policy.megatron_config.transformer_config_kwargs.fp8_recipe=blockwise \
trainer.ref.megatron_config.transformer_config_kwargs.fp8_recipe=blockwise \
trainer.policy.megatron_config.transformer_config_kwargs.fp8_amax_compute_algo=most_recent \
trainer.ref.megatron_config.transformer_config_kwargs.fp8_amax_compute_algo=most_recent \
trainer.policy.megatron_config.transformer_config_kwargs.tp_only_amax_red=false \
trainer.ref.megatron_config.transformer_config_kwargs.tp_only_amax_red=false \
trainer.micro_train_batch_size_per_gpu=2 \
trainer.micro_forward_batch_size_per_gpu=2 
generator.sampling_params.max_generate_length=8192 \
generator.n_samples_per_prompt=8 \
generator.eval_n_samples_per_prompt=16 \
generator.inference_engine.max_num_seqs=32 \
generator.eval_sampling_params.max_generate_length=8192 trainer.algorithm.use_kl_loss=false \
generator.inference_engine.num_engines=8 \
generator.inference_engine.tensor_parallel_size=1 \
generator.inference_engine.data_parallel_size=1 \
generator.inference_engine.expert_parallel_size=1 \
generator.inference_engine.engine_init_kwargs.quantization=fp8 \
generator.inference_engine.fp8_weight_sync_mode=serialized_blockwise \
generator.inference_engine.engine_init_kwargs.kv_cache_dtype=fp8_e4m3 \

B200 (only show the difference):

export FLA_TILELANG=0                          # B200/sm_100: TileLang GDN kernels crash; use triton
export NVTE_FP8_BLOCK_SCALING_FP32_SCALES=0    # B200: pow-2 (MXFP8) block scales — mandatory
export NVTE_FP8_BLOCK_AMAX_EPSILON=1e-4 

trainer.policy.model.path=Qwen/Qwen3.5-35B-A3B \
trainer.policy.megatron_config.tensor_model_parallel_size=1 \
trainer.policy.megatron_config.expert_model_parallel_size=8 \
trainer.policy.megatron_config.expert_tensor_parallel_size=1 \
trainer.ref.megatron_config.tensor_model_parallel_size=1 \
trainer.ref.megatron_config.expert_model_parallel_size=8 \
trainer.policy.megatron_config.transformer_config_kwargs.fp8_param=false \
trainer.ref.megatron_config.transformer_config_kwargs.fp8_param=false\

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces blockwise FP8 serialization for rollout weight synchronization (specifically targeting Qwen3.5 layouts), adds chunking to vocab_parallel_entropy to bound temporary memory usage, and implements a colocated worker memory barrier with hard eviction to manage HBM footprint. It also updates sequence packing and alignment logic for FP8/TP/CP layouts and supports persistent FP8 parameter training. The review feedback highlights two critical issues: first, the bytes_per_token calculation for vocab entropy chunking does not account for batch dimensions, which could lead to OOMs; second, accessing megatron_config directly in prepare_runtime_environment will crash FSDP training runs due to an AttributeError, requiring defensive guards.

Comment thread skyrl/backends/skyrl_train/distributed/megatron/model_utils.py Outdated
Comment thread skyrl/train/utils/utils.py Outdated
@YJHMITWEB
YJHMITWEB force-pushed the fp8-rl-weight-sync branch from 9113224 to 5c463c2 Compare July 14, 2026 19:24
@jinghanyao1-hub jinghanyao1-hub self-assigned this Jul 14, 2026
@YJHMITWEB
YJHMITWEB force-pushed the fp8-rl-weight-sync branch 4 times, most recently from 2b1a0a3 to 787a4c5 Compare July 17, 2026 00:26
@erictang000 erictang000 self-assigned this Jul 20, 2026

@erictang000 erictang000 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed as a distributed-systems pass (correctness / concurrency / distributed-sync / numerical-stability / memory / perf). FP8 quantization numerics are sound (fuzz-verified), the lossy-master-seed window is correctly closed, and the BF16 path is preserved. Four issues below: one crash-on-valid-input (High) and three latent distributed hazards (Medium) that surface on specific hardware/config combos. Line-anchored comments follow.

Also worth confirming against the pinned Megatron/Bridge (couldn't verify here): (1) fp8_param.py:152 calls _copy_model_params_to_main_params(state_dict=...) but stock Megatron's method takes no args and copies from live (quantized) params — assumes a patched Bridge version; (2) the hybrid path re-seeds param_to_fp32_param/CPU copies but not optimizer.state[param]['master_param'], which megatron_strategy.py's own patch treats as authoritative; (3) the 3D fused-MoE weight_loader fast path in new_inference_worker_wrap.py assumes vLLM accepts a batched 3D tensor in one call.

Comment thread skyrl/train/dataset/collators.py
Comment thread skyrl/backends/skyrl_train/workers/megatron/fp8_param.py
torch.cuda.ipc_collect()
torch.cuda.synchronize()
for logical_chunk in chunks:
for chunk in iter_single_dtype_chunks(logical_chunk):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Per-chunk IPC collective count is now dtype-content-dependent → deadlock if ranks' dtype composition diverges.

iter_single_dtype_chunks yields one sub-chunk per distinct dtype (first-seen order), and _send_single_dtype_chunk_vllm_native issues an unconditional all_gather_object + 2 barrier per sub-chunk. So the per-logical-chunk collective count now equals the number of distinct dtypes in that chunk on that rank. Pre-PR this count was purely structural (bucket layout, identical across ranks); it is now content-dependent. If any rank's logical chunk has a different dtype set/order (e.g. weight+scale on one rank vs weight-only on another), ranks call all_gather_object a different number of times and hang.

This relies on extract_weights (out of scope) producing perfectly rank-symmetric dtype composition; the transport takes it on faith. The intra-sub-chunk if any(tensor.dtype != dtype): raise guard is dead code and does not protect against cross-rank divergence.

Fix: before the loop, all_gather_object each rank's per-logical-chunk sub-chunk dtype list and assert equality (fail fast instead of hanging), or drive the partition from a rank-0 broadcast. At minimum document the invariant.


if fp8_contract_enabled:
fp8_env_defaults["NVTE_FP8_BLOCK_SCALING_FP32_SCALES"] = scale_mode
if serialized_fp8 and scale_mode == "1":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Power-of-two (Blackwell) serialized path neither defaults nor validates VLLM_USE_DEEP_GEMM_E8M0 — asymmetric with the FP32 path → silent numeric mismatch.

This scale_mode == "1" (FP32) branch both rejects a conflicting VLLM_USE_DEEP_GEMM_E8M0 != "0" and pins the default to "0", so TE and vLLM agree. The scale_mode == "0" power-of-two branch (the documented Blackwell case) does neither — the var is only forwarded if already present in the driver env.

Scenario: Blackwell run sets NVTE_FP8_BLOCK_SCALING_FP32_SCALES=0 + serialized_blockwise, fp8_param=false (passes all config checks) but forgets VLLM_USE_DEEP_GEMM_E8M0=1. TE emits E8M0 power-of-two scales; vLLM interprets them without E8M0 → mis-dequantized weights → silently degraded rollouts, no error. The symmetric FP32 mistake is caught.

Fix: handle scale_mode == "0" symmetrically — default VLLM_USE_DEEP_GEMM_E8M0=1 and reject a conflicting != "1".

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YJHMITWEB this seems correct, shouldn't export VLLM_USE_DEEP_GEMM_E8M0 be 1 for blackwell? or is there some reason that it doesn't work w/ blockwise + power of 2?

not seeing VLLM_USE_DEEP_GEMM_E8M0=1 in the config that you're using as well so curious about this

@erictang000

Copy link
Copy Markdown
Collaborator

@jinghanyao1-hub ai code review above, you can ignore for now, still taking a deeper pass, will resolve the above if i find it's not reasonable

@YJHMITWEB
YJHMITWEB force-pushed the fp8-rl-weight-sync branch from 787a4c5 to 8460420 Compare July 21, 2026 19:46
@erictang000

Copy link
Copy Markdown
Collaborator

high level comment on file structure:

can we reorganize to create a new folder workers/megatron/quantization/ that contains the _fp8_block_amax_epsilon_patch.py (maybe rename to just amax_epsilon_patch.py, and fp8_param.py? We can also move fake_int4_qat.py into that folder. So something like:

workers/megatron/quantization/
    __init__.py
    amax_epsilon_patch.py    
    fp8_param.py      
    fake_int4_qat.py

and then can we split the serialized_fp8.py file into a separate folder/components inside weight_sync? The structure should be something like this

weight_sync/fp8/
    __init__.py            
    quantize.py           # blockwise_cast_to_fp8, batched_blockwise_cast_to_fp8, scale helpers
    vllm_format.py        # SERIALIZED_BLOCKWISE_FP8, wire prefix, scale_name_for_weight,
                          #   get_serialized_fp8_quantization_config, iter_serialized_fp8_tensors
    models/qwen35.py      # _QWEN35_* tables, is_qwen35_config, ignored layers, MoE spec

@erictang000

Copy link
Copy Markdown
Collaborator

another high level comment:

can we add a gpu ci test in the style of test_megatron_models but for full fp8? This will be added to H100 CI.

Essentially this test:

but with fp8 quantization on both vllm and megatron.

This will help us maintain full fp8 as a tracked feature across transformer engine/megatron/vllm updates. We want to test vllm/megatron init, weight sync, and logprob diff. We can start with qwen3.5-0.8b and qwen3.5-35B-a3B tests with blockwise quantization only.

@erictang000 erictang000 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left some comments:

  1. let's design a more extensible/maintainable solution for model specific quantization mappings/logic and try to keep instances of using a model name to a single config/abstraction
  2. i'm a little confused by the fp8 param init logic but i might be missing something
  3. ideally we can find areas where we could upstream logic to megatron-bridge rather than maintaining it ourselves for things like quantizing weights to fp8 for transfer
  4. gpu ci tests for both regular full fp8 and fp8 params for hopper (+ both moe and dense tests for full fp8, so 3 total gpu ci tests)

otherwise the weight sync changes for cuda ipc/nccl look pretty clean, looking forward to seeing the PR with updates!


if fp8_contract_enabled:
fp8_env_defaults["NVTE_FP8_BLOCK_SCALING_FP32_SCALES"] = scale_mode
if serialized_fp8 and scale_mode == "1":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YJHMITWEB this seems correct, shouldn't export VLLM_USE_DEEP_GEMM_E8M0 be 1 for blackwell? or is there some reason that it doesn't work w/ blockwise + power of 2?

not seeing VLLM_USE_DEEP_GEMM_E8M0=1 in the config that you're using as well so curious about this

# rank's local token slab to be 16-aligned; globally this means
# ``16*cp_size``.
# - FP8 requires 16-token local slabs for TP=1. With sequence
# parallelism, TE quantizes all-gather inputs in 128-token blocks,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this 128 number is specific to blockwise quantization right? can we clarify the wording to reflect that

Suggested change
# parallelism, TE quantizes all-gather inputs in 128-token blocks,
# parallelism, TE with fp8_recipe=blockwise quantizes all-gather inputs in 128-token blocks,


model_dtype: str = "bfloat16"
"""Should match the dtype used by the inference engine."""
fp8_weight_sync_mode: Optional[str] = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


from loguru import logger

_ENV_VAR = "NVTE_FP8_BLOCK_AMAX_EPSILON"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quick question for my understanding: so this is a custom environment variable that we introduced? I can't find it anywhere inside TransformerEngine or elsewhere in OSS

And this is needed because with power of 2 scaling (which is needed on blackwell) + blockwise is not well supported in TE, and doesn't handle CP/TP packing where we can get a block of all 0s

And the reason existing recipes on TransformerEngine using Float8BlockScaling are able to run without issue is because they all use hopper? And everyone is primarily is using MXFP8BlockScaling on blackwell where there is no amax_epsilon concept?

)

provider = bridge.to_megatron_provider()
# Defer persistent-FP8 checkpoint import until the bridge can expose

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so the problem here is that:

if fp8 param is enabled, then megatron bridge will not store the unquantized weights?

looking through megatron bridge i'm seeing that on to_megatron_provider if load_weights=true, then we call load_weights_hf_to_megatron, which has the following logic to store the unquantized state dict in the bridge: https://github.com/NVIDIA-NeMo/Megatron-Bridge/blob/47254ca622d35b8063fcc780b070588836ddf20a/src/megatron/bridge/models/conversion/model_bridge.py#L1072

is there a reason we need to defer it from here to manually calling self.bridge.load_hf_weights(self.actor_module)? might be missing something, pls correct if my understanding is wrong? Otherwise i think the _release_fp8_param_unquantized_state and initialize_fp8_param_optimizer_masters makes sense to me since we do need to pass that state dict to initialize the optimizer state.

) -> Iterator[tuple[str, torch.Tensor]]:
"""Yield vLLM checkpoint tensors for one Megatron-exported weight."""

if batched_moe_expert_spec(name) is not None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq: if we have fp8 param on, are the quantization operations here no-ops? or are we unconditionally re-quantizing?

if so should we handle fp8 param case separately? (potentially using the interface from this pr: NVIDIA-NeMo/Megatron-Bridge#1994 )

@@ -0,0 +1,385 @@
"""Convert Megatron-exported tensors to vLLM's blockwise FP8 checkpoint format."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one high level thought: there is this PR in megatron-bridge: NVIDIA-NeMo/Megatron-Bridge#1994 that was merged more than 3 months ago that introduces a new function: build_export_fp8_tasks - this is designed for the case where we have fp8 params, and allows the user to ship fp8 params + scale metadata directly to vLLM

ideally we could push down the logic in this file for exporting an iterable list of fp8 params to Megatron-Bridge as well and just add support for the case where we have bf16 params but we want to export them to fp8.

return epsilon


_QWEN35_FP8_WEIGHT_SUFFIXES = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mentioned this in the previous comment about re-organizing files, but these mappings should definitely be in a separate file if we have them at all, and the references to fp8 weight suffixes, up/down suffixes, and unquantized suffixes should be made generic behind some abstraction.

Ideally which layers are quantized could be configured, but we can leave that as a follow up of this PR. we should at least design the interface that defines the quantization spec rather than hard coding.

Overall some abstraction like this:

@dataclass(frozen=True)
class ModelFp8Spec:
    matches: Callable[[Any], bool]              # hf_config -> bool (replaces is_qwen35_config)
    should_quantize: Callable[[str, Sequence[int]], bool]   # name/shape -> quantize?
    ignored_layers: Callable[[Any], list[str]]  # hf_config -> vLLM ignored modules (residual shape/vLLM denylist, e.g. vision proj)
    moe_expert_spec: Callable[[str], MoeExpertSpec | None]  # gate/up split + expert batching

@dataclass(frozen=True)
class MoeProjection:
    hf_name: str          # "gate_proj" | "up_proj" | "down_proj"  (what the sender emits on the wire)
    vllm_param: str       # ".experts.w13_weight" | ".experts.w2_weight"  (vLLM fused target, weight only)
    shard_id: str         # "w1" | "w3" | "w2"
    split: Optional[tuple[int, int]] = None   # (chunk_index, num_chunks) along split_dim; None = whole tensor

@dataclass(frozen=True)
class MoeExpertSpec:
    source_suffix: str    # ".mlp.experts.gate_up_proj" | ".mlp.experts.down_proj"  (Bridge-exported batched name)
    split_dim: int        # dim to carve projections from the fused [E, ...] tensor (1 = per-expert output rows)
    projections: tuple[MoeProjection, ...]

QWEN35_MOE_EXPERT_SPECS = (
    MoeExpertSpec(
        source_suffix=".mlp.experts.gate_up_proj",
        split_dim=1,
        projections=(
            MoeProjection("gate_proj", ".experts.w13_weight", "w1", split=(0, 2)),  # first half
            MoeProjection("up_proj",   ".experts.w13_weight", "w3", split=(1, 2)),  # second half
        ),
    ),
    MoeExpertSpec(
        source_suffix=".mlp.experts.down_proj",
        split_dim=1,
        projections=(
            MoeProjection("down_proj", ".experts.w2_weight", "w2"),  # whole tensor
        ),
    ),
)

FP8_MODEL_SPECS: list[ModelFp8Spec] = [QWEN35_FP8_SPEC, ...]

def resolve_fp8_spec(hf_config) -> ModelFp8Spec | None:
    return next((s for s in FP8_MODEL_SPECS if s.matches(hf_config)), None)

can you help propose some more modular per model abstraction along these lines? Also seeing if any of mappings from megatron-bridge like here: https://github.com/NVIDIA-NeMo/Megatron-Bridge/blob/b3a5514340af701340e34b9ec506d92688d2a1b9/src/megatron/bridge/models/qwen/qwen35_bridge.py#L187 can be reused would go a long way in making this more extensible/maintainable in the future

from skyrl.backends.skyrl_train.weight_sync import get_transfer_strategy
from skyrl.backends.skyrl_train.weight_sync.serialized_fp8 import (
SERIALIZED_BLOCKWISE_FP8,
get_qwen35_fp8_ignored_layers,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here it would be great to remove any mention of specific models and use some abstraction for layers to ignore - we shouldn't hard code things for qwen3.5 in here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants